# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import time
import logging
import json
from typing import Optional

from tornado.ioloop import PeriodicCallback
from tornado import gen
from recordclass import recordclass
import requests

from coin.base.atomic import AtomicLong
from coin.base.timestamp import get_timestamp
from coin.exchange.kraken.kr_rest.native_private_client import KrakenNativePrivateClient
import coin.flow.topic_map as topic_map

NumberOfCalls = recordclass(
    'NumberOfCalls',
    ['query_account_balance', 'query_open_orders', 'query_closed_orders', 'query_trades_history'])


class CallCounter(object):
  def __init__(self, max_counter):
    self._max_counter = max_counter
    self._counter = AtomicLong(0)
    self._last_query_time = 0
    self._num_calls = NumberOfCalls(
        query_account_balance=AtomicLong(0),
        query_open_orders=AtomicLong(0),
        query_closed_orders=AtomicLong(0),
        query_trades_history=AtomicLong(0),
    )
    self._periodic_call = None
    self._logger = logging.getLogger(__name__)

  def dec(self):
    value = self._counter.value - 1
    if value >= 0:
      self._counter.get_and_set(value)
    if self._counter.value > self._max_counter - 5:
      self._logger.info('call_counter = %s', self._counter.value)

  def is_ready_to_query(self, query_method_name, *, query_follows=False, increase_by=1):
    if self._counter >= self._max_counter:
      return False

    timestamp = get_timestamp()
    if timestamp - self._last_query_time < 10e6:
      return False

    if getattr(self._num_calls, query_method_name) == min(self._num_calls):
      if query_follows:
        self._last_query_time = get_timestamp()
        self._counter.get_and_add(increase_by)
        atomic_num = getattr(self._num_calls, query_method_name)  # AtomicLong
        atomic_num.get_and_add(increase_by)
        self._logger.debug('%s is called!', query_method_name)
      return True

    return False


# A util function that is tightly coupled with KrakenPrivateQueryer.
def _create_private_query_coroutine(query_method_name, increase_by, query_period_attr):
  @gen.coroutine
  def wrapper(self):
    if not self._call_counter.is_ready_to_query(
        query_method_name, query_follows=True, increase_by=increase_by):
      return

    try:
      if wrapper.__name__ in ('query_closed_orders', 'query_trades_history'):
        start = max(int(time.time()) - 600, self._start_time)
        response = yield getattr(self._private_client, query_method_name)(start=start)
      else:
        response = yield getattr(self._private_client, query_method_name)()

      if response.status_code in (502, 520):
        self._logger.error('Invalid status code: %s', response.status_code)
        return
      elif response.status_code != 200:
        self._logger.error('Invalid status code: %s (%s)', response.status_code, response.content)
        return
      msg = {
          'query': query_method_name,
          'data': json.loads(response.content),
      }
      self._publisher.write(get_timestamp(), msg)
    except json.JSONDecodeError:
      self._logger.exception('JsonDecodeException: %s', response.content)
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException')
    except Exception:
      self._logger.exception('Unknown Exception')
      raise

  wrapper.counter_multiplier = increase_by
  wrapper.query_period_attr = query_period_attr
  wrapper.__name__ = query_method_name
  return wrapper


class KrakenPrivateQueryer(object):
  _query_account_balance = _create_private_query_coroutine('query_account_balance',
                                                           1,
                                                           'account_query_period')
  _query_open_orders = _create_private_query_coroutine('query_open_orders',
                                                       1,
                                                       'orders_snapshot_query_period')
  _query_closed_orders = _create_private_query_coroutine('query_closed_orders',
                                                         1,
                                                         'dead_orders_query_period')
  _query_trades_history = _create_private_query_coroutine('query_trades_history',
                                                          2,
                                                          'fill_query_period')

  def __init__(self,
               *,
               queue,
               worker_id: str,
               private_client: KrakenNativePrivateClient,
               logger=None):
    self._private_client = private_client

    topic_id = topic_map.search_topic_id('kraken_rest_private_%s' % worker_id)[0][1]
    self._publisher = queue.get_writer(topic_id=topic_id)

    self._queryers = []
    self._start_time = int(time.time())
    self._call_counter = None
    self._periodic_call_counter_reducer = None
    self._logger = logger or logging.getLogger(__name__)

  @property
  def query_methods(self):
    return [
        self._query_account_balance,
        self._query_open_orders,
        self._query_closed_orders,
        self._query_trades_history
    ]

  def __del__(self):
    self.stop()
    self._publisher.close()

  def start(self, ioloop, og_config, later_td: Optional[float] = 0.0):
    adjust_factor = min(12, self._private_client.num_of_keys)
    self._call_counter = CallCounter(max_counter=og_config.kraken_max_call_counter)
    reduce_period = og_config.kraken_call_counter_reduce_period * 1000 / adjust_factor
    self._periodic_call_counter_reducer = PeriodicCallback(self._call_counter.dec, reduce_period)
    self._periodic_call_counter_reducer.start()

    for query_method in self.query_methods:
      query_period = getattr(og_config, query_method.query_period_attr) * 1000
      queryer = PeriodicCallback(query_method, query_period)
      self._queryers.append(queryer)
      ioloop.call_later(later_td, queryer.start)

  def stop(self):
    if self._periodic_call_counter_reducer is not None:
      self._periodic_call_counter_reducer.stop()
    for queryer in self._queryers:
      queryer.stop()
